<?php if (!defined('CORE_DIR')) die('Wrong entry point!');

/* 
 * Copyright 2010 Anton Muraviev a.k.a kuromaisu
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */

Aero::define('FileFormControl', AS_CLASS);

/*
 * Constants: Default file control styles
 * 
 * DEFAULT_FILE_UPLOAD		-	displayed no files has been uploaded;
 * DEFAULT_FILE_DELETE		-	displayed when one file has been uploaded.
 * 
 */
define('DEFAULT_FILE_UPLOAD','<input '.DEFAULT_FORM_ATTRIBUTES.' type="file"> <input type="submit" name="{upload_command}" value="{upload_label}">');
define('DEFAULT_FILE_DELETE','<input type="submit" name="{delete_command}" value="{delete_label}">');
define('DEFAULT_FILE_DESCRIPTION','<span class={file_type_class}>{file_name} ({file_size_readable})</span>');


/* -----------------------------------------------------------------------------
 * Class: FileFormControl
 * -----------------------------------------------------------------------------
 *
 */
class FileFormControl extends BaseFormControl
{
	protected $storage_path = '';
	protected $storage_depth = 0;
	
	public function __construct($params = array())
	{
		$this->properties = array_merge($this->properties, array('storage_path','storage_depth'));
		
		parent::__construct($params);
	}
	
	public function validate()
	{
		return !$this->hasErrors();
	}
	
	public function fileChecksum($file)
	{
		return sha1($this->storage_path . $file['name'] . $file['location'] . $file['size'] . $file['type'] . AeroForm::secret()); 
	}
	
	public function hasValidChecksum($file)
	{
		return isset($file['name'],$file['location'],$file['type'],$file['size'])
			&& $this->fileChecksum($file) === $file['_cs_'];
	}
	
	protected function _initialize()
	{
		parent::_initialize();
		
		AeroForm::registerCommand('_upload_'.$this->name.'_', '_delete_'.$this->name.'_');
		
		$this->snippets['control'] = '[file_list] [file_upload]';
		$this->dynamic_snippets = array('file_list','file_upload');
		
		// Setup valid upload path
		if (empty($this->storage_path))
		{
			$this->storage_path = Aero::option('form.file.storage_path',ini_get('upload_tmp_dir'));
		}
		
		$this->storage_path = aero_real_path($this->storage_path);
		
		if ($this->storage_path === false
			|| !is_dir($this->storage_path)
			|| !is_writable($this->storage_path))
		{
			trigger_error('There is a problem with the upload path: <kbd>'.$storage_path.'</kbd> ' . ($this->storage_path === false ? ' cannot be resoled.' : ' is not writable.'), E_USER_ERROR);
		}
		
		// Check file checksums
		if ($this->multiple) foreach($this->value as $index => $file)
		{
			if (!$this->hasValidChecksum($file))
			{
				unset($this->value[$index]);
			}
		}
		else if (!$this->hasValidChecksum($this->value))
		{
			$this->value = null;
		}
		
		// Handle file uploads and deletions
		$Request = Aero::load('libs/request');
		
		if ($Request->has('file',$this->name))
		{
			// FIXME: Handle uploads even if offset has not been supplied.
			$this->_handleUpload($this->_realOffset($Request->data('_upload_'.$this->name.'_')));
		}
		
		if ($Request->has('post','_delete_file_'))
		{
			$this->_handleDelete($this->_realOffset($Request->data('_delete_'.$this->name.'_')));
		}
	}
	
	protected function _realOffset($value)
	{
		if (is_array($value))
		{
			$value = array_keys($value);
			return array_pop($value);
		}
		
		return false;
	}
	
	protected function _modifyVariables(&$vars)
	{
		$vars['upload_command'] = '_upload_'.$this->name.'_' .($this->multiple ? '['.$vars['offset'].']' : '');
		$vars['delete_command'] = '_delete_'.$this->name.'_' .($this->multiple ? '['.$vars['offset'].']' : '');
		$vars['upload_label'] = 'Upload';
		$vars['delete_label'] = 'Delete';
	}
	
	protected function _handleUpload($offset)
	{
		$Request = Aero::load('libs/request');
		$files = $Request->files($this->name);
		
		if (empty($files))
		{
			return;
		}
		
		foreach ($files as $key => $file)
		{
			if (is_numeric($offset) && $offset !== $key)
			{
				continue;
			}
			
			if ($file['error'] !== UPLOAD_ERR_OK)
			{
				switch ($file['error'])
				{
					case UPLOAD_ERR_INI_SIZE:
					case UPLOAD_ERR_FORM_SIZE:
						$message = 'file_upload_size_error';
						break;
					case UPLOAD_ERR_PARTIAL:
						$message = 'file_upload_internal_error';
						break;
					case UPLOAD_ERR_NO_FILE:
						continue 2;
						break;
					case UPLOAD_ERR_NO_TMP_DIR:
					case UPLOAD_ERR_CANT_WRITE:
					case UPLOAD_ERR_EXTENSION:
					default:
						throw new Exception('File upload failed with code <var>int('.$file['error'].')</var> due to server error');
				}
				
				Aero::import('libs/i18n');
				
				if (is_numeric($offset))
				{
					$this->error[$offset] = ae('constraints', $message);
				}
				else
				{
					$this->error = ae('constraints', $message);
				}
				
				continue;
			}
			
			$_file = pathinfo($file['name'], PATHINFO_BASENAME);
			$_parts = explode('.',$_file);
			
			$file_ext = count($_parts) > 1 ? array_pop($_parts) : '';
			$file_name = implode('.',$_parts);
			$file_version = '';
			
			// Should we use a tree of nested directories to store the file?
			// This is useful when you deal with millions of files.
			if ($this->storage_depth > 0)
			{
				$storage = str_split(md5($file_name));
				$storage = array_slice($storage, 0, min(32,$this->storage_depth));
				$location = implode('/',$storage).'/';
			}
			else
			{
				$storage = array();
				$location = '';
			}
			
			
			// Mangle file name
			$location.= sha1($file_name . AeroForm::secret());
			
			// Files must never be overwritten!
			while (file_exists($this->storage_path . '/' . $location . $file_version . '.' . $file_ext))
			{
				$file_version = (int)$file_version + 1;
			}
			
			$location.= $file_version . '.' . $file_ext;
			
			// Create all storage directories.
			if (count($storage) > 0 && !is_dir($this->storage_path.'/'.implode('/',$storage)))
			{
				// FIXME: Use chmod() on each directory instead of relying on umask.
				$old = umask(0);
				mkdir($this->storage_path.'/'.implode('/',$storage), 0777, true);
				umask($old);
			}
			
			// Move the file
			if (move_uploaded_file($file['tmp_name'],$this->storage_path.'/'.$location) !== false)
			{
				unset($file['tmp_name']);
				
				$file['location'] = $location;
				
				$this->_addFile($file,$offset);
			}
			// If failed save an error
			else 
			{
				Aero::import('libs/i18n');
				
				if (is_numeric($offset))
				{
					$this->error[$offset] = ae('constraints', 'file_upload_failed');
				}
				else
				{
					$this->error = ae('constraints', 'file_upload_failed');
				}
			}
		}
	}
	
	protected function _handleDelete($offset)
	{
		$this->_removeFile($offset);
	}
	
	protected function _addFile($file,$offset)
	{
		if (is_numeric($offset))
		{
			$this->value[$offset] = $file;
		}
		else
		{
			$this->value = $file;
		}
	}
	
	protected function _removeFile($offset)
	{
		if (is_numeric($offset))
		{
			$location = isset($this->value[$offset]['location']) ? $this->value[$offset]['location'] : '';
			unset($this->value[$offset]);
		}
		else
		{
			$location = isset($this->value['location']) ? $this->value['location'] : '';
			$this->value = array();
		}
		
		if ($location && file_exists($this->storage_path . '/' . $location))
		{
			unlink($this->storage_path . '/' . $location);
		}
	}
	
	// ==================
	// = Control layout =
	// ==================
	
	protected function _fileUploadSnippet(&$variables)
	{
		$file = $this->_fieldProperty('value', true, $variables['offset']);
		
		if (!empty($file) && is_array($file))
		{
			return '';
		}
		
		return Aero::option('form.'.$this->style.'.file_upload', DEFAULT_FILE_UPLOAD);
	}
	
	protected function _fileListSnippet(&$variables)
	{
		$file = $this->_fieldProperty('value', true, $variables['offset']);
		
		if (empty($file) || !is_array($file))
		{
			return '';
		}
		
		return $this->_file($file) . Aero::option('form.'.$this->style.'.file_delete', DEFAULT_FILE_DELETE);
	}
	
	protected function _file(&$file)
	{
		$template = Aero::option('form.'.$this->style.'.file_description', DEFAULT_FILE_DESCRIPTION) .'
			<input type="hidden" name="{name}[name]" value="{file_name}" />
			<input type="hidden" name="{name}[location]" value="{file_location}" />
			<input type="hidden" name="{name}[size]" value="{file_size}" />
			<input type="hidden" name="{name}[type]" value="{file_type}" />
			<input type="hidden" name="{name}[_cs_]" value="{file_checksum}" />';
		
		$vars = array(
			'{file_checksum}' => $this->fileChecksum($file),
			'{file_name}' => $file['name'],
			'{file_location}' => $file['location'],
			'{file_size}' => $file['size'],
			'{file_size_readable}' => $file['size'] . ' bytes', // TODO: File size should be human readable. 
			'{file_type}' => $file['type'],
			'{file_type_class}' => preg_replace('~[^a-z0-9]+~i', '-', $file['type']),
		);
		
		$vars = self::escape($vars);
		
		return str_replace(array_keys($vars), array_values($vars), $template);
	}
}